
package galcon.visuals;

import galcon.players.AI;
import galcon.players.Command;
import galcon.players.Human;
import galcon.players.Player;
import galcon.utils.Circle;
import galcon.utils.Constants;
import galcon.visuals.components.Background;
import galcon.visuals.components.Fleet;
import galcon.visuals.components.Planet;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JPanel;

public class GameEngine extends JPanel implements ActionListener
{
    protected Planet [] planets;
    protected Player[] players;
    protected ArrayList<Fleet> fleets;

    protected long lastPlanetGrowTime;
    protected Random rand;
    protected int numPlanets;
    protected int humanIndex;

    public GameEngine() {
        initGraphics();
        initGame();
    }
    
    private void initGraphics() {
        setPreferredSize (new Dimension (Constants.WIDTH, Constants.HEIGHT));
        setFocusable (true);
    }
    
    private void initGame() {
        this.rand = new Random(System.currentTimeMillis());
        initPlayers();
        initPlanets();
        initFleets();
    }

    private void initFleets(){
        this.fleets = new ArrayList<Fleet>();
    }

    private void initPlayers(){
        int numPlayers = Constants.TOTAL_AIS;
        if(Constants.HUMAN_ENABLED){
            numPlayers++;
        }

        this.players = new Player[numPlayers];

        int currentIndex = 0;
        int currentID = 1;

        if(Constants.HUMAN_ENABLED){
            this.players[currentIndex] = new Human(currentID, Color.BLUE);
            this.humanIndex = currentIndex;
            initHuman();
            currentIndex++;
            currentID++;
        }else{
            this.humanIndex = -1;
        }

        for(;currentIndex < this.players.length; ++currentIndex){
            this.players[currentIndex] = new AI(currentID, Color.RED);
            currentID++;
        }
    }

    private void initHuman(){
        this.addMouseListener((Human) players[this.humanIndex]);
        this.addMouseWheelListener((Human) players[this.humanIndex]);
        this.addMouseMotionListener((Human) players[this.humanIndex]);
    }
    
    private void initPlanets() {

        lastPlanetGrowTime = -1;

        int row = Constants.WIDTH / Constants.SQUARE_PIXEL_SIZE;
        int col = Constants.HEIGHT / Constants.SQUARE_PIXEL_SIZE;
        int size = row * col;
        planets = new Planet [size + players.length];
        
        for (int i = 0; i < row; i++)
            for (int j = 0; j < col; j++) {
                Planet temp; boolean ok;
                do {
                    temp = generatePlanet(i, j);
                    ok = true;
                    if (i > 0 && planets [(i - 1) * col + j].getCircle().intersects (temp.getCircle())) ok = false;
                    if (j > 0 && planets [i * col + j - 1].getCircle().intersects (temp.getCircle()))   ok = false;
                    if (i > 0 && j > 0 && planets [(i - 1) * col + j - 1].getCircle().intersects(temp.getCircle())) ok = false;
                    if (i > 0 && j < col - 1 &&  planets [(i - 1) * col + j + 1].getCircle().intersects (temp.getCircle())) ok = false;
                } while (!ok); // expected number of additional iterations is basically zero
                temp.addShips (rand.nextInt (Constants.MAX_NEUTRAL_PLANET_SHIPS));
                planets [i * col + j] = temp;
            }
        
        // generate home planets
        int index = size;
        for (Player p : players) {
            Planet temp;
            boolean ok;
            int count = 0;
            do {
                temp = generateHomePlanet (p.getPlayerID());
                ok = true;
                for (int j = 0; j < index; j++)
                    if (temp.getCircle().intersects (planets[j].getCircle())) {
                        ok = false;
                        break;
                    }
            } while (!ok);
            temp.setColor (p.getColor());
            planets [index++] = temp;
        }
    }
    
    private Planet generatePlanet(int lox, int loy) {
        int radius, x, y;
        do {
            radius = rand.nextInt (Constants.MAX_PLANET_SIZE - Constants.MIN_PLANET_SIZE) + Constants.MIN_PLANET_SIZE;
            x = rand.nextInt (Constants.SQUARE_PIXEL_SIZE) + lox * Constants.SQUARE_PIXEL_SIZE;
            y = rand.nextInt (Constants.SQUARE_PIXEL_SIZE) + loy * Constants.SQUARE_PIXEL_SIZE;
        } while (x - radius <= 0 || y - radius <= 0 || x + radius >= Constants.WIDTH || y + radius >= Constants.HEIGHT);
        
        galcon.utils.Point p = new galcon.utils.Point (x, y);
        Circle c = new Circle (p, radius);
        return new Planet (c, Constants.NEUTRAL_OWNER_ID, radius + getRandomError (Constants.PLANET_PRODUCTION_ERROR_RANGE));
    }
    
    private Planet generateHomePlanet(int ownerID) {
        int radius = Constants.MAX_PLANET_SIZE;
        int x = rand.nextInt (Constants.WIDTH - radius * 2) + radius;
        int y = rand.nextInt(Constants.HEIGHT - radius * 2) + radius;
        
        galcon.utils.Point p = new galcon.utils.Point (x, y);
        Circle c = new Circle (p, radius);
        Planet ret = new Planet (c, ownerID, radius + Constants.PLANET_PRODUCTION_ERROR_RANGE + 1);
        ret.addShips (Constants.INITIAL_STARTING_SHIPS);
        return ret;
    }
    
    private int getRandomError (int range) {
        return rand.nextInt(2 * range) - range;
    }
    
    public void actionPerformed (ActionEvent e) {
        tickGame();
        repaint();
    }
    
    public void paintComponent (Graphics g) {
        super.paintComponent (g);
        paintBackground(g);
        paintPlanets(g);

        if(Constants.HUMAN_ENABLED){
            ((Human)this.players[this.humanIndex]).drawCursor(g);
        }

    }

    private void tickGame(){
        
        //Check for first run
        if(this.lastPlanetGrowTime == -1){
            //First time ticking, set grow time to now
            this.lastPlanetGrowTime = System.currentTimeMillis();
            tickPlanetGrowth();
        }

        //Other try to tick normally
        long currentTime = System.currentTimeMillis();
        if(currentTime - this.lastPlanetGrowTime > Constants.PLANET_GROWTH_TIME_MILLIS){
            tickPlanetGrowth();
            this.lastPlanetGrowTime = currentTime;
        }

        //Tick fleets
        tickFleets();

        //Execute Commands
        Command[] commands = getCommands();

        for(int i = 0; i < commands.length; i++){
            if(commands[i] != null)
                executeCommand(commands[i]);
        }
    }

    private void tickFleets(){
        for(int i = 0; i < fleets.size(); i++){
            fleets.get(i).step();
        }
    }

    private void executeCommand(Command command){
        if (command == null) return;

        int[] sourcePlanetIDs = command.getSourcePlanetIDs();
        int[] destPlanetIDs = command.getDestPlanetIDs();
        int[] sizeFleets = command.getSizeFleets();

        
        // If it has reached here, the command is valid for owner id and not null
        
        Player player = command.getPlayer();


        //Verify Command integraty
        try{

            //Null pointers
            if(sourcePlanetIDs == null ||
                    destPlanetIDs == null ||
                    sizeFleets == null){
                throw new IllegalArgumentException("Null pointer recieved from command.");
            }

            //Size of arrays
            int size = sourcePlanetIDs.length;
            if(size != destPlanetIDs.length ||
                    size != sizeFleets.length){
                throw new IllegalArgumentException("Length of command arrays are different.");
            }

            //Check all ints are positive
            for(int i = 0; i < size; i++){
                if(sourcePlanetIDs[i] < 0 ||
                        destPlanetIDs[i] < 0 ||
                        sizeFleets[i] < 0){
                    throw new IllegalArgumentException("ID of planet in command was negative.");
                }
            }

            //Check ownerIDs on source planets
            for(int i = 0; i < sourcePlanetIDs.length; i++){
                int planetID = sourcePlanetIDs[i];
                if(this.planets[planetID].getOwnerID() != player.getPlayerID());{
                    throw new IllegalArgumentException("Source planet has different ownerID than player.");
                }
            }

            //Execute actual command

            for(int i = 0; i < size; i++){
                int sourcePlanetID = sourcePlanetIDs[i];
                int destPlanetID = destPlanetIDs[i];
                int sizeFleet = sizeFleets[i];

                //Check if if sourcePlanets have enough ships
                //If it does output as a warning, but continue
                if(this.planets[sourcePlanetID].getShips() < sizeFleets[i]){
                    System.out.println("WARNING: SOURCE PLANET HAS LESS THAN SENT NUMBER, WILL SEND MAX.");
                    sizeFleet = this.planets[sourcePlanetID].getShips();
                }

                //It is possible for a planet to have zero ships
                this.planets[sourcePlanetID].subtractShips(sizeFleet);

                this.fleets.add(new Fleet(sizeFleet, this.planets[destPlanetID], this.planets[sourcePlanetID], player.getPlayerID(), Color.WHITE));
            }

        }catch(Exception e){
            System.out.println(e.getMessage());
        }

    }

    private Command[] getCommands(){
        Command[] commands = new Command[players.length];

        //Make sure to copy the planets and fleets so they can't be descructive
        //They can return null, we have to check later
        //They don't know the moves of the other AI until the next tick



        for(int i = 0; i < commands.length; i++){

            Fleet[] temp_fleets = new Fleet[fleets.size()];
            temp_fleets = fleets.toArray(temp_fleets);
            Planet[] temp_planets = new Planet[planets.length];
            for(int j = 0; j < this.planets.length; ++j){
                temp_planets[j] = this.planets[j].clone();
            }

            commands[i] = players[i].getCommand(temp_planets, temp_fleets);

            //Verify it is the correct issuer of the command, don't want any sneaky hackers
            if(commands[i] != null){
                try{
                    if(commands[i].getPlayer() == null){
                        throw new IllegalArgumentException("Command recieved from a player has a null player value.");
                    }

                    if(commands[i].getPlayer().getPlayerID() != players[i].getPlayerID()){
                        throw new IllegalArgumentException("Command recieved from a player that did not match their owner id.");
                    }
                }catch(Exception e){
                    //This better never happen
                    System.out.println(e.getMessage());
                    commands[i] = null;
                }
            }
        }
        
        return commands;
    }

    private void tickPlanetGrowth(){
        //Tick all planets to grow if they are not neutral
        for(int i = 0; i < planets.length; i++){
            if(planets[i].getOwnerID() != Constants.NEUTRAL_OWNER_ID){
                planets[i].addShips(Constants.PLANET_GROWTH_SHIPS);
            }
        }
    }
    
    private void paintBackground (Graphics g) {
        Background.getInstance().draw (g);
    }
    
    private void paintPlanets (Graphics g) {
        for (Planet p : planets)
            p.draw(g);
    }

}
